# ImgDist
# accept, drop and distribute images 
# - sockets and http for communication
# - PNG and JPEG preferred as images

try:
	import BaseHTTPServer
except:
	import http.server as BaseHTTPServer
try:
	import SocketServer
except:
	import socketserver as SocketServer
try:
	from urllib2 import urlopen
except:
	from urllib.request import urlopen
import socket
try:
	import thread
except:
	import _thread as thread
import threading
import time
try:
	import StringIO as io
except:
	import io

HTTP_INPORT  = 8081 
HTTP_OUTPORT = 8082
SOCKET_PORT  = 9081
BLOCK_SIZE   = 1024

LOG = "LOG"
DIST = "DIST"
ACCEPT = "ACCEPT"


### The image store

class ImgStore(object):
    """Stores one image and removes it after delivery. 
    Multiple stores overwrite existing images. Thread-safe.
    Supports blocking get.
    """

    def __init__(self):
        self.img = None
        self.condition = threading.Condition()

    def put(self, img):
        "Put a binary blob into to store, overwrite eventually"
        self.condition.acquire()
        self.img = img # potentially overwrites!
        self.condition.notify()
        self.condition.release()

    def get(self):
        "Get a stored binary blob. Returns None, if none is available"
        self.condition.acquire()
        img = self.img
        self.img = None
        self.condition.release()
        return img

    def get_wait(self): 
        "Get a stored binary blob. Block/wait, until one is available"
        self.condition.acquire()
        while self.img == None:
            self.condition.wait()
        img = self.img 
        self.img = None
        self.condition.release()
        return img

    def peek(self):
        "As get, but keeps the image"
        self.condition.acquire()
        img = self.img
        self.condition.release()
        return img

# a global store
img_store = ImgStore()


### Observers

class Observers(object):
    "Thread-safe collection of observers"

    def __init__(self):
        self.observers = []
        self.lock = threading.Lock()
    
    def add(self, func):
        "add a callable as observer"
        self.lock.acquire()
        try:
            if func not in self.observers:
                self.observers.append(func)
        except Exception as data:
            print("Observer: failed to add_observer", data)
        self.lock.release()

    def remove(self, func):
        "remove a callable observer, if it is available, else do nothing"
        self.lock.acquire()
        try:
            if func in self.observers:
                self.observers.remove(func)
        except Exception as data:
            print("Observer: failed to remove_observer", data)
        self.lock.release()

    def get_all(self):
        "gets a list (copy) of all callable observers"
        self.lock.acquire()
        ret = self.observers[:]
        self.lock.release()
        return ret

    def apply(self, *params):
        "calls/applies the observers with given params"
        for observer in self.get_all():
            observer(*params)


class ObserversDict(object):
    "Collection of observers differentiated by name, a dict of observers"

    def __init__(self, *names):
        self.dic = dict()
        for name in names:
            self.dic[name] = Observers()
    
    def add(self, name, func):
        "add an observer under a given name"
        if name not in self.dic:
            self.dic[name] = Observers()
        self.dic[name].add(func)

    def remove(self, name, func):
        "removes an observer under a given name"
        if name in self.dic:
            self.dic[name].remove(func)

    def get_all(self, name):
        "get all observers of a given name"
        if name in self.dic:
            return self.dic[name].get_all()
        return []

    def apply(self, name, *params):
        "calls/applies the observers of a given name"
        if name in self.dic:
            return self.dic[name].apply(*params)
        return None


### Helpers

def parse_query(path):
    "filename as cmd and GET parameters as dictionary"
    params = dict()
    pathparams = path.split('?', 1)
    if len(pathparams) == 1:
        return path.strip('/'), params
    path = pathparams[0].strip('/')
    for entry in pathparams[1].split('&'): 
        entry = entry.split('=', 1)
        params[entry[0]] = entry[1]
    return path, params

import Image
def adjust_image(img, params):
    if "width" in params and "height" in params:
        # twice StringIO; a little heavy weight?
        width = int(params["width"])
        width = width if width >= 10 else 10
        height = int(params["height"])
        height = height if height >= 10 else 10
        sio = io.StringIO(img)
        image = Image.open(sio)
        resized_image = image.resize((width, height))
        sio = io.StringIO()
        img_format = "JPEG" 
        if "type" in params:
            if params["type"] == "PNG":
                img_format = "PNG"
                # nothing else supported
        resized_image.save(sio, format=img_format, optimize=1)
        img = sio.getvalue()
    return img

def int2bstr(val):
    "platform independent int to bitstring"
    return (chr((val & 0xFF000000)>>24) + 
            chr((val & 0xFF0000)>>16) + 
            chr((val & 0xFF00)>>8) + 
            chr((val & 0xFF)))

def bstr2int(val):
    "platform independent bitstring to int"
    if len(val) != 4:
        raise ValueError
    return ((ord(val[0])<<24) + 
            (ord(val[1])<<16) +
            (ord(val[2])<<8) +
            (ord(val[3])))
    


### Socket based Image Distribution 

class ImgDistHandlerSocket(SocketServer.BaseRequestHandler):
    "Handler class for image acception and distribution via Sockets"
    observers = ObserversDict()

    def handle(self):
        "accepts and processes one of a number of commands as query string"
        cmds = {'quit': self.nothing, 
                'get' : self.get,
                'put' : self.put,
                }
        while True:
            # expects a path and query string (as in HTTP)
            query = self.request.recv(1024)
            if not query:
                self.log("ignoring empty command")
                continue
            if "\n" not in query:
                self.log("query too long or short: ", query)
                raise
            query, _ = query.split("\n", 1)
            self.log("parsing", query)
            cmd, params = parse_query(query)
            peer = str(self.request.getpeername())
            self.log("got", cmd, "with params", params, "from", peer)
            if cmd == "quit":
                break
            if cmd in cmds:
                apply(cmds[cmd], (params, rest))
            else:
                self.log("cmd", cmd, "not found")
            self.log("command processed")
        self.request.close()

    def nothing(self, params, rest):
        "command: empty callback"
        pass

    def quit(self, params, rest):
        "command: quit, should it really?"
        pass

    def get(self, params, rest):
        "command: gets and writes one image from the image store, blocking"
        img = img_store.get_wait()
        img = adjust_image(img, params)
        self.log("has image", len(img))
        self.write_data(img)
        self.observers.apply(DIST, img)

    def put(self, params, rest):
        "command: puts one image into the image store"
        if "size" not in params:
            self.log("error - put: didn't receive a size")
            return
        expected_length = int(params["size"])
        received = [rest]
        act_length = len(rest)
        while True:
            if expected_length - act_length < BLOCK_SIZE:
                buf = self.request.recv(BLOCK_SIZE)
            else:
                buf = self.request.recv(expected_length-act_length)
            received.append(buf)
            act_length += len(buf)
            if act_length >= expected_length:
                break
        if act_length != expected_length:
            self.log("error receiving")
            raise Exception("error receiving")
        img = "".join(received)
        ret = img_store.put(img)
        self.observers.apply(ACCEPT, img)
        return ret

    def write_data(self, data):
        "sends data out starting with a 4 byte length"
        length = int2bstr(len(data)) 
        self.request.send(length)
        res = self.request.sendall(data)
        if res != None:
            self.log("Damn, error sending", res)

    def log(self, *pos):
        "log its arguments"
        text = " ".join(map(str, pos))
        self.observers.apply(LOG, text)


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    "just a mixin for a threaded server"
    pass


class ImgDistSocket(object):
    "image acception and distribution via sockets"

    def __init__(self, port=SOCKET_PORT, handler=ImgDistHandlerSocket):
        self.addr = ('', port)
        self.handler = handler
        self.observers = self.handler.observers
        self.server = None
        self.server_thread = None

    def start(self):
        "Starts the socket server in a separate thread"
        if self.server:
            self.server.shutdown()
            self.server = None
            self.server_thread = None
        self.server = ThreadedTCPServer(self.addr, self.handler)
        self.server_thread = threading.Thread(target=self.server.serve_forever)
        self.server_thread.setDaemon(True)
        self.server_thread.start()

    def stop(self):
        "Stops the socket server"
        if self.server:
            self.server.shutdown()
            self.server = None
            self.server_thread = None


### HTTP based image distribution

class ImgDistHandlerHTTP(BaseHTTPServer.BaseHTTPRequestHandler):
    "Handles image acception and distribution via HTTP"
    observers = ObserversDict()

    def do_GET(self):
        "serves the next available picture, blocks if none is available"
        path, params = parse_query(self.path)
        img = self.get_image(params)
        self.send_response(200)
        self.send_header("Content-Type", "image/jpg")
        self.send_header("Content-Length", str(len(img)))
        self.end_headers()
        self.wfile.write(img)        
        self.observers.apply(DIST, img)

    def get_image(self, params):
        "gets an image"
        img = img_store.get_wait()
        img = adjust_image(img, params)
        self.log("has image", len(img))
        return img

    def do_POST(self):
        "accepts the next picture to be distributed"
        path, params = parse_query(self.path)
        clen = int(self.headers["Content-Length"])
        img = self.rfile.read(clen)
        self.log("got posted image", len(img))
        img_store.put(img)
        self.log("put posted image into store")
        self.send_response(200)
        self.end_headers()
        self.observers.apply(ACCEPT, img)

    def log(self, *pos):
        "log arguments"
        text = " ".join(map(str, pos))
        self.observers.apply(LOG, text)

    def log_message(self, *pos):
        "do not do any default logging on stderr"
        pass


class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
    "http://code.activestate.com/recipes/425210/"

    def server_bind(self):
        "start listening with timeout"
        BaseHTTPServer.HTTPServer.server_bind(self)
        self.socket.settimeout(1)
        self.run = True

    def get_request(self):
        "get a request, but timeout and quit if no longer needed"
        while self.run:
            try:
                sock, addr = self.socket.accept()
                sock.settimeout(None)
                return (sock, addr)
            except socket.timeout:
                pass # ignore timeouts

    def stop(self):
        "stops the listening after next timeout"
        self.run = False

    def serve(self):
        "the main loop"
        while self.run:
            self.handle_request()


class ImgDistHTTP(object):
    "image acception and distribution via HTTP"

    def __init__(self, port=HTTP_OUTPORT, handler=ImgDistHandlerHTTP):
        self.port = port
        self.addr = ('', self.port)
        self.handler = handler
        self.observers = self.handler.observers
        self.httpd = None

    def start(self):
        "starts the HTTP server"
        self.httpd = StoppableHTTPServer(self.addr, self.handler)
        thread.start_new_thread(self.httpd.serve, ())

    def stop(self):
        "stops the HTTP server"
        if self.httpd:
            self.httpd.stop()
            self.httpd = None


# For application development
class ImgSenderSocket(object):
    """Send image data to the socket-based ImgDist Image Distributer.
    Make an instance and call the instance with a binary image (PNG/JPEG).
    Don't forget to close!
    """

    def __init__(self, host="localhost", port=SOCKET_PORT):
        self.addr = (host, port)
        self.sock = None
        self.reset()

    def send(self, img):
        "send image to running socket server on an open connection"
        img_size = len(img)
        query = "put?size="+str(img_size)+"\n"
        if not self.sock:
            return # ignore, closing
        res = self.sock.sendall(query)
        if res != None:
            print("ImgSender send: query send not ok")
            raise Exception
        res = self.sock.sendall(img)
        if res != None:
            print("ImgSender send: img send not ok")
            raise Exception            

    def __call__(self, img):        
        self.send(img)

    def reset(self):
        "reopen connection"
        self.close()
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(self.addr)
        self.sock = sock
    
    def close(self):
        "close connection"
        if self.sock:
            sock = self.sock
            sock = None
            try:
                sock.close()
            except:
                pass # if closing fails, just hope


class ImgSenderHTTP(object):
    """Send an image to the HTTP-based ImgDist Image Distributer.
    Make an instance and call the instance with a binary image (PNG/JPEG).
    """

    def __init__(self, host="localhost", port=HTTP_INPORT):
        self.addr = (host, port)

    def send(self, img):
        "send image to running socket server on an open connection"
        img_size = len(img)
        query = "put?size="+str(img_size)+"\n"
        url = "http://%s:%s/%s" % (self.addr + (query,))
        # print("httpsend", url)
        urlopen(url, img)
        # print("httpsenddone", url)

    def __call__(self, img):        
        self.send(img)

    def close(self):
        "closing not needed for HTTP, but adhere to interface"
        pass

